第二章 shell程序设计

更详细的说明:http://www.tldp.org/HOWTO/Bash-Prog-Intro-HOWTO.html


为什么使用shell编程

原因之一是,可以快速、简单地完成编程。而且最基本的Linux安装也会提供一个shell。

shell非常适合于编写一些执行相对简单的任务的小工具,因为它们强调的是易于配置、易于维护和可移植性,而不是很看重执行效率。

使用shell可以使命令按照预定顺序执行。

一点哲学

UNIX架构非常依赖于代码的高度可重用性。Linux提供了各种各样小巧而简单的工具,比如:

$ ls -al | more

通常可以将许多小巧的脚本程序组合起来以创建一个庞大而复杂的程序。

什么是shell

shell是一个作为用户与Linux系统间接口的程序,它允许用户向操作系统输入需要执行的命令。

shell程序和其他程序环绕在Linux内核的四周。

shell程序有很多种,在Linux系统中,标准shell是GNU工具集中的bash。使用如下命令查看bash的版本号:

$ /bin/bash --version

管道和重定向

重定向输出

把Linux程序(不仅仅是shell程序)的输出保存到文件:

$ ls -l > lsoutput.txt

需要知道的是:文件描述符0代表一个程序的标准输入,文件描述符1代表标准输出,文件描述符2代表标准错误输出。

>操作符把标准输出重定向到一个文件。默认情况下,如果该文件已经存在,它的内容将被覆盖。

使用>>操作符将输出内容附加到一个文件的尾部。例如:

$ ps >> lsoutput.txt

使用2>操作符可以重定向标准错误输出。

下面的命令将把标准输出和标准错误输出分别重定向到不同的文件中:

$ kill -HUP 1234 >killout.txt 2>killerr.txt

如果想把两组输出都重定向到一个文件中,可以使用>&操作符来结合两个输出,如:

$ kill -HUP 1234 >killout.txt 2>&1

这条命令的含义是:将标准输出重定向到文件killout.txt,然后将标准错误输出重定向到与标准输出相同的地方。

这样可以丢弃所有的输出信息:

$ kill -HUP 1234 >/dev/null 2>&1

/dev/null是Linux通用的“回收站”。

重定向输入

使用<操作符,如:

$ more < killout.txt

但这样做意义不大,因为more命令可以接受文件名作为参数。

管道

可以用管道操作符|来连接进程。Linux下通过管道连接的进程可以同时运行,并且随着数据流在它们之间的传递可以自动地进行协调。

举一个列子,可以使用sort命令对ps命令的输出进行排序:

$ ps | sort

如果有一系列的命令需要执行,相应的输出文件是在这一组命令被创建的同时立刻被创建或写入的,所以决不要在命令流中重复使用相同的文件名。比如:

cat mydata.txt | sort > mydata.txt

最终将得到一个空文件,因为在读取文件mydata.txt之前就已经覆盖了这个文件的内容。

作为程序设计语言的shell

编写shell脚本程序有两种方式:输入一系列命令让shell交互地执行他们;把这些命令保存到一个文件中,然后将该文件作为一个程序来调用。

交互式程序

即在命令行上直接输入shell脚本。当shell期待进一步的输入时,正常的$shell提示符将改变为>提示符,由shell来判断何时输入完毕并立即执行脚本程序。

比如:

$ for file in *
> do
> if grep -l POSIX $file
> then
> more file
> fi
> done

grep -l找到当前路径下包含POSIX字符串的文件,more命令将文件的内容显示在屏幕上。

Note

shell 编程里,*可以做到自动扩展的功能,这里扩展成当前工作目录下的所有的文件名字(以空格分隔)。又比如,echo *将输出当前工作目录下所有的文件的名字。

扩展的花括号{}允许将任意字符串放在一个集合中,以供shell进行扩展,比如:

$ ls my_{finger,toe}s

将列出文件my_fingers和my_toes。

创建脚本

一个简单的模板(见first.sh):

#! /bin/bash

## filename: ...
## description: ...

shell语句

exit 0

程序中的注释以#号开始,一直持续到该行的结束。

#! /bin/bash是一种特殊形式的注释,#!字符告诉系统紧跟在它后面的那个参数是用来执行本文件的程序(使用绝对路径)。

因为脚本程序本质上被看作是shell的标准输入,所以它可以包含任何能够通过你的PATH环境变量引用到的Linux命令。

exit命令的作用是确保脚本程序能够返回一个有意义的退出码,表示此脚本是否执行成功。在shell程序设计里,0表示成功。

Linux很少利用文件扩展名来决定文件的类型,可以为脚本使用.sh或其他扩展名,但shell并不关心这一点。大多数预安装的脚本程序并没有使用任何文件扩展名。检查文件是否是脚本程序的最好方法是使用file命令,如:file first.sh

Note

一个有趣的写法是:#! /bin/bash -x ,这将在执行脚本的同时,输出命令和其参数内容。

把脚本设置为可执行

运行脚本程序的方法是:

$ sh fist.sh

也可以只输入脚本名字就运行程序,这需要给脚本文件加上可执行权限:

$ chmod +x first.sh
$ ./fist.sh

在确信你的脚本程序能够正确执行后,你可以把它从当前目录移动到一个更合适的地方去,比如,家目录的 bin 目录。如果想给其他人使用,可以移动到 /usr/local/bin 目录。

如果要移动到 /usr/local/bin 目录,就需要有 root 权限。比如使用 root 执行这样的命令:

# cp first /usr/local/bin
# chwon root /usr/local/bin/first
# chgrp root /usr/local/bin/first
# chmod 755 /usr/local/bin/first

或者这样执行 chmod 命令赋予权限:

# chmod u=rwx,go=rx /usr/local/bin/first

shell的语法

变量

在shell里,使用变量之前无需为它们做出声明,通过使用它们(比如给它们赋初值时)来创建它们。

默认情况下,所有的变量都被看作字符串并以字符串来存储。shell和一些工具程序会在需要时把数值型字符串转换为对应的数值。

变量区分大小写。

在变量名前加一个$符号来访问它的内容。比如:

$ salutation=Hello
$ echo $salutation

如果字符串里包含空格,就必须用引号把它们括起来;等号两边不能有空格。

使用read命令将用户的输入赋值给一个变量,通常情况下,在用户按下回车键时,read命令结束。如:

$ read a
hello world
$ echo $a
hello world

使用引号

一般情况下,脚本文件的参数以空白字符分隔,如果想在一个参数中包含空白字符,必须给参数加上引号。

如果把一个$变量表达式放在双引号中,程序执行到这一行时就会把变量替换为它的值;如果把它放在单引号中,就不会发生替换现象。在$符前面加上\字符以取消它的特殊含义。

环境变量

当一个shell脚本程序开始执行时,一些变量会根据环境设置中的值进行初始化。这些变量通常用大写字母做名字,用户在脚本里定义的变量通常用小写字母做名字。

主要的环境变量有:

环境变量 说明
$PATH 搜索命令的目录列表
$HOME 用户宿主目录
$PS1 命令提示符
$PS2 二级提示符,一般是>
$IFS 输入域分隔符,比如空格符,换行符
$0 shell脚本的名字
$# 传递给脚本的参数个数
$$ 脚本的进程ID,可用于生成一个唯一的文件
$! 本脚本中上一个执行的进程pid

参数变量

如果脚本程序在调用时带有参数,一些额外的变量就会被创建:

参数变量 说明
$*$@ 列出所有的参数,更推荐使用$@,因为$*会根据$IFS进行分隔
$1$2, ... 第n个参数

条件

一个shell脚本能够对任何可以从命令行上调用的命令的退出码进行测试。

test或[命令

在实际工作中,大多数脚本程序都会广泛使用shell的布尔判断命令test或[。test命令的退出码(表明条件是否满足)决定是否需要执行后面的条件代码。

为了可读性,当使用[命令时,还可以使用符号]来结尾。

一个用户检查文件是否存在的例子:

if test -f fred.c
then
...
fi

或者写成:

if [ -f fred.c ]
then
...
fi

test命令可以使用的条件类型可以归为3类:字符串比较、算术比较和文件有关的条件测试。

一些条件如下表:

字符串比较 结果
string1 = string2 如果字符串相等,返回真
string1 != string2 如果字符串不相等,返回真
-n string 如果字符串不为空则返回真
-z string 如果字符串为空则返回真
算术比较 结果
expr1 -eq expr2 两个表达式相等,返回真
expr1 -nq expr2 两个表达式不相等。返回真
expr1 -gt expr2 表达式1大于表达式2,返回真
expr1 -ge expr2 表达式1大于等于表达式2,返回真
expr1 -lt expr2 表达式1小于表达式2,返回真
expr1 -le expr2 表达式1小于等于表达式2,返回真
文件条件测试 结果
-d file 如果file是目录,则为真
-f file 如果file存在,则为真
-s file 如果file大小不为0,则为真

test命令是bash的内置命令,使用help test命令可以获得test命令的详细信息(或者man test)。

my note

Linux 提供了两个内置命令 true 和 false ,分别可以返回真和假的结果。

控制结构

if语句

它对某个命令的执行结果进行测试,然后根据测试结果有条件地执行一组语句。如:

if condition
then
    statements
elif condition
then
    statements
else
    statements
fi

for语句

可以利用for结构来循环处理一组值,这组值可以是任意字符串的集合。它们可以在程序里被列出,更常见的做法是使用shell的文件名扩展结果。

简单语法是:

for variable in values
do
    statements
done

while语句

如果需要重复执行一个命令序列,但事先又不知道这个命令序列应该执行的次数,你通常会使用一个while循环,它的语法如下:

while condition
do
    statements
done

until语句

语法如下:

until condition
do
    statements
done

循环将反复执行,直到条件为真。

case语句

语法如下:

case variable in
    partion [ | pattern] ...) statements;;
    partion [ | pattern] ...) statements;;
    ...
esac

case结构允许你通过一种比较复杂的方式将变量的内容和模式进行匹配,然后再根据匹配的模式去执行不同的代码。

每个模式行都以双分号结尾。

case将使用第一个匹配的模式,即使后续的模式有更加精确的匹配也是如此。

命令列表

AND和OR专门用于处理命令列表。

AND列表结构允许你按照这样的方式执行一系列命令,只有在前面的所有命令执行成功的情况下,才执行后一条命令:

statement1 && statement2 && statement3

只有列表中的所有命令都执行成功时,才算它执行成功,否则就算它失败。

OR列表结构允许我们执行一系列命令直到有一条命令成功为止,其后的命令将不再被执行:

statement1 || statement2 || statement3

这些列表类型结构的执行方式被称为短路求值(short circuit evaluation)

还可以把这两种结构结合在一起,比如:

[ -f file ] && cmd1 || cmd2

上面的语句中,如果file存在,则执行cmd1,否则执行cmd2。还可以使用括号来强制求值的顺序。

语句块

如果想在某些只允许使用单个语句的地方使用多条语句,你可以把它们括在花括号中来构造一个语句块。

函数

定义一个函数的语法如下:

function_name() {
    statements
}

当程序遇到函数定义时,它会记住此函数名,然后从}字符之后的位置继续执行。当执行到function_name时,shell就知道应该去执行刚才定义的函数了。这个函数执行完毕以后,执行过程会返回到调用函数那条语句的后面继续执行。

当一个函数被调用时,脚本程序的位置参数($*, $@, $#, $1, $2)等会被替换为函数的参数,这也是读取传递给函数的参数的办法。当函数执行完毕后,这些参数会恢复为它们先前的值。

可以通过return命令让函数返回数字值。如果想让函数返回字符串,可以让函数将字符串保存在一个变量中,该变量然后可以在函数结束之后使用。或者可以echo一个字符串并捕获其结果。如:

foo() { echo JAY; }
...
result = "$(foo)"

如果在函数里没有使用return指定一个返回值,函数返回的就是执行的最后一条命令的退出码。

可以使用local关键字在shell函数中声明局部变量,使其仅在函数内有效。函数可以访问全局作用范围内的其他shell变量。

here文档

在shell脚本程序中向一条命令传递输入的一种特殊方法是使用here文档。它允许一条命令在获得输入数据时就好像是在读取一个文件或键盘一样,而实际上是从脚本程序中得到输入数据。

更多见代码案例以及书本。

命令

在shell脚本程序内部,可以执行两类命令:一类是可以在命令提示符中执行的“普通”命令,也称为外部命令(external command),一类是“内置”命令,也称为内部命令(internal command)。

内置命令是在shell内部实现的,不能作为外部程序被调用。然而,大多数的内置命令同时也提供了独立运行的程序版本。

break命令

用于跳出for, while或until循环。

:命令

冒号命令是一个空命令。

它偶尔会被用于简化条件逻辑,相当于true的一个别名。while :实现了一个无限循环,可代替更常见的while true

: 结构也会被用在变量的条件设置中,例如:

${var:=value}

continue命令

使for, while或until循环跳到下一次循环继续执行。循环变量取值循环列表中下一个值。

.命令

点命令用于在当前shell中执行命令:

. ./shell_script

当一个脚本执行一条外部命令或脚本程序时,它会创建一个新的环境(一个子shell),命令将在这个新环境中执行,在命令执行完毕后,这个环境被丢弃,留下退出码返回给父shell。

但外部的source命令和点命令在执行脚本中列出的命令时,使用的是调用该脚本程序的同一个shell。

点命令允许执行的脚本程序改变当前环境。

echo命令

一个常见的问题是如何去掉换行符,Linux常用的解决方法是:

echo -n "string to output"

还可以:

echo -e "string to output\c"

echo -e确保启用了反斜线转义字符(\c代表去掉换行符,\t代表制表符,\n代表回车)。

eval命令

eval命令允许你对参数进行求职。见书本。

exec命令

典型用法是将当前shell替换为一个不同的程序。如:

exec wall "Thanks for all the fish"

exit n命令

exit命令使脚本程序以退出码n结束运行。如果在任何一个交互式shell的命令提示符中使用这个命令,它会使你退出系统。

如果你允许自己的脚本程序在退出时不指定一个退出状态,那么该脚本中最后一条被执行命令的状态将被用作返回值。

在shell脚本编程中,退出码0表示成功,1~125是脚本程序可以使用的错误代码。其余数字具有保留含义。

Note

如果 exit 没有参数,则表示 exit 0 。

export命令

export命令将作为它参数的变量导出到子shell中,并使之在子shell中有效。

默认情况下,在一个shell中被创建的变量在这个shell调用的下级shell中是不可用的。export命令把自己的参数创建为一个环境变量,而这个环境变量可以被当前程序调用的其他脚本和程序看见。

expr命令

expr把参数当成表达式来进行算术运算,然后输出运算结果。对于比较运算,如果为真返回1,否则返回0。

expr支持的运算有:四则运算,求余,比较运算(=,>, <)等。

printf命令

语法是:

printf "format string" param1 param2

转义序列和转换说明见书本。

return命令

return命令的作用是使函数返回。return命令有一个数值参数的返回值。如果没有指定参数,return命令默认返回最后一条命令的退出码。

set命令

set命令的作用是为shell设置参数变量。

shift命令

shift命令把所有参数变量左移一个位置,使$2变成$1,$3变成$2,以此类推。原来的$1将被丢弃,而$0仍将保持不变。

如果脚本程序需要10个或10个以上的参数,就需要使用shift命令来访问第十个及其后面的参数。

trap命令

trap命令用于指定在接收到信号后将要采取的行动。

可以使用trap -l来查看信号编号及其关联的名称。

信号是指那些被异步发送到一个程序的事件。在默认情况下,它们通常会终止一个程序的运行、

trap命令有两个参数,第一个参数是接收到指定信号时将要采取的行动,第二个参数是要处理的信号名:

trap command signal

如果想重置某个信号的处理方式到其默认值,只需将command设置为-。如果要忽略某个信号,就把command设置为空字符串''。

unset命令

unset命令的作用是从环境中删除变量或函数。

find命令

find命令是一个用于搜索文件的命令。如:

$ find / -name test -print

这个命令的含义是:从根目录开始查找名为test的文件,并且输出该文件的完整路径。

find命令的完整语法格式是:

find [path] [options] [tests] [actions]

path既可以是绝对路径,也可以是相对路径,如果需要,还可以指定多个路径,如:find /var /home

find命令有许多options可用,见书本。

可以提供给find命令的测试非常多,测试返回的结果有true或false。find命令开始工作时,它按照顺序将定义的每种测试依次应用到它搜索到的每个文件上。如果一个测试返回false,find命令就停止处理它当前找到的找个文件,并继续搜索。如果一个测试返回true,find命令将继续下一个测试或对当前文件采取行动。

常用测试见书本。

发现匹配指定条件的文件之后,就可以对其执行动作了。魔术字符串{}是-exec或-ok命令的一个特殊类型的参数,它被当前文件的完整路径所取代。见书本。

grep命令与正则表达式

grep代表的是通用正则表达式解析器(General Regular Expression Parser)。grep命令在文件中搜索字符串。

grep命令的语法如下:

grep [options] PATTERN [FILES]

如果没有提供文件名,grep命令将搜索标准输入。

grep的一些主要的选项如下:

选项 含义
-c 输出匹配行的数目,而不是输出匹配的行
-E 启用扩展表达式
-h 取消每个输出行的普通前缀,即匹配查询模式的文件名
-i 忽略大小写
-l 只列出包含匹配行的文件名,而不输出真正的匹配行
-v 对匹配模式取反,即搜索不匹配行而不是匹配行

正则表达式

正则表达式允许你实现更复杂的匹配。

正则表达式中的一些字符是以特定方式处理的,见书本。

命令的执行

获取命令输出

有两种方法可以获得命令输出的结果:

算术扩展

除了可以使用expr命令获得算术运算的输出结果,还可以使用如下方法:

$((...))

...中的可以是某些算术运算。如果引用了变量,那么不需要在前面加$来引用。

这样做更加灵活,因为可以算术运算内容可以是一个字符串,运算符两边不需要加空格,还可以进行多项式的运算。

但,这个方法只适合做整数运算,不能得到小数的结果,如:echo $((3 / 4)) 得到的是0 。

可以使用 bc 命令突破这种限制:echo 3 / 4 | bc -l

参数扩展(Parameter Expansion)

你可以在shell中采用多种参数替换方法。对于多参数处理问题来说,这些方法通常会提供一种精巧的解决方案。

参数扩展 说明
${param:-default} 如果param为空,就把它设置为default的值
${#param} 给出param的长度
${param%word} 从param的尾部开始删除与word匹配的最小部分,然后返回剩余部分
${param%%word} 从param的尾部开始删除与word匹配的最长部分,然后返回剩余部分
${param#word} 从param的头部开始删除与word匹配的最小部分,然后返回剩余部分
${param##word} 从param的头部开始删除与word匹配的最长部分,然后返回剩余部分
${param//pattern/string} 对所有的匹配执行替换

虽然看上去比较难理解,但如果结合例子就很容易了。例子在书本p60以及代码笔记。

还有一种扩展方法如:

${i}_tmp,这种方法会把i扩展,而保留tmp。假如i=1,扩展的结果是1_tmp。